home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
PC World 2008 September
/
PCWorld_2008-09_cd.bin
/
v cisle
/
sadanastroju
/
lightning-0.8-tb-win.xpi
/
js
/
calRecurrenceInfo.js
< prev
next >
Wrap
Text File
|
2007-12-03
|
27KB
|
764 lines
/* -*- Mode: javascript; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is lightning code.
*
* The Initial Developer of the Original Code is
* Oracle Corporation
* Portions created by the Initial Developer are Copyright (C) 2005
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Vladimir Vukicevic <vladimir.vukicevic@oracle.com>
* Matthew Willis <lilmatt@mozilla.com>
* Daniel Boelzle <daniel.boelzle@sun.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
function calRecurrenceInfo() {
this.mRecurrenceItems = new Array();
this.mExceptions = new Array();
}
function calDebug() {
dump.apply(null, arguments);
}
var calRecurrenceInfoClassInfo = {
getInterfaces: function (count) {
var ifaces = [
Components.interfaces.nsISupports,
Components.interfaces.calIRecurrenceInfo,
Components.interfaces.nsIClassInfo
];
count.value = ifaces.length;
return ifaces;
},
getHelperForLanguage: function (language) {
return null;
},
contractID: "@mozilla.org/calendar/recurrence-info;1",
classDescription: "Calendar Recurrence Info",
classID: Components.ID("{04027036-5884-4a30-b4af-f2cad79f6edf}"),
implementationLanguage: Components.interfaces.nsIProgrammingLanguage.JAVASCRIPT,
flags: 0
};
calRecurrenceInfo.prototype = {
// QI with CI
QueryInterface: function(aIID) {
if (aIID.equals(Components.interfaces.nsISupports) ||
aIID.equals(Components.interfaces.calIRecurrenceInfo))
return this;
if (aIID.equals(Components.interfaces.nsIClassInfo))
return calRecurrenceInfoClassInfo;
throw Components.results.NS_ERROR_NO_INTERFACE;
},
//
// Mutability bits
//
mImmutable: false,
get isMutable() { return !this.mImmutable; },
makeImmutable: function() {
if (this.mImmutable)
return;
for each (ritem in this.mRecurrenceItems) {
if (ritem.isMutable)
ritem.makeImmutable();
}
for each (ex in this.mExceptions) {
if (ex.item.isMutable)
ex.item.makeImmutable();
}
this.mImmutable = true;
},
clone: function() {
var cloned = new calRecurrenceInfo();
cloned.mBaseItem = this.mBaseItem;
var clonedItems = [];
for each (ritem in this.mRecurrenceItems)
clonedItems.push(ritem.clone());
cloned.mRecurrenceItems = clonedItems;
var clonedExceptions = [];
for each (exitem in this.mExceptions) {
var c = exitem.item.cloneShallow(this.mBaseItem);
clonedExceptions.push( { id: exitem.id, item: c } );
}
cloned.mExceptions = clonedExceptions;
return cloned;
},
//
// calIRecurrenceInfo impl
//
mBaseItem: null,
get item() {
return this.mBaseItem;
},
set item(value) {
if (this.mImmutable)
throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
this.mBaseItem = value;
// patch exception's parentItem:
for each (exitem in this.mExceptions) {
exitem.item.parentItem = value;
}
},
mRecurrenceItems: null,
get isFinite() {
if (!this.mBaseItem)
throw Components.results.NS_ERROR_NOT_INITIALIZED;
for each (ritem in this.mRecurrenceItems) {
if (!ritem.isFinite)
return false;
}
return true;
},
getRecurrenceItems: function(aCount) {
if (!this.mBaseItem)
throw Components.results.NS_ERROR_NOT_INITIALIZED;
aCount.value = this.mRecurrenceItems.length;
return this.mRecurrenceItems;
},
setRecurrenceItems: function(aCount, aItems) {
if (!this.mBaseItem)
throw Components.results.NS_ERROR_NOT_INITIALIZED;
if (this.mImmutable)
throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
// should we clone these?
this.mRecurrenceItems = aItems;
},
countRecurrenceItems: function() {
if (!this.mBaseItem)
throw Components.results.NS_ERROR_NOT_INITIALIZED;
return this.mRecurrenceItems.length;
},
getRecurrenceItemAt: function(aIndex) {
if (!this.mBaseItem)
throw Components.results.NS_ERROR_NOT_INITIALIZED;
if (aIndex < 0 || aIndex >= this.mRecurrenceItems.length)
throw Components.results.NS_ERROR_INVALID_ARG;
return this.mRecurrenceItems[aIndex];
},
appendRecurrenceItem: function(aItem) {
if (!this.mBaseItem)
throw Components.results.NS_ERROR_NOT_INITIALIZED;
if (this.mImmutable)
throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
this.mRecurrenceItems.push(aItem);
},
deleteRecurrenceItemAt: function(aIndex) {
if (!this.mBaseItem)
throw Components.results.NS_ERROR_NOT_INITIALIZED;
if (this.mImmutable)
throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
if (aIndex < 0 || aIndex >= this.mRecurrenceItems.length)
throw Components.results.NS_ERROR_INVALID_ARG;
this.mRecurrenceItems.splice(aIndex, 1);
},
deleteRecurrenceItem: function(aItem) {
if (!this.mBaseItem)
throw Components.results.NS_ERROR_NOT_INITIALIZED;
if (this.mImmutable)
throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
// Because xpcom objects can be wrapped in various ways, testing for
// mere == sometimes returns false even when it should be true. Use
// the interface pointer returned by sip to avoid that problem.
var sip1 = Components.classes["@mozilla.org/supports-interface-pointer;1"]
.createInstance(Components.interfaces.nsISupportsInterfacePointer);
sip1.data = aItem;
sip1.dataIID = Components.interfaces.calIRecurrenceItem;
for (var i = 0; i < this.mRecurrenceItems.length; i++) {
if (this.mRecurrenceItems[i] == sip1.data) {
this.deleteRecurrenceItemAt(i);
return;
}
}
throw Components.results.NS_ERROR_INVALID_ARG;
},
insertRecurrenceItemAt: function(aItem, aIndex) {
if (!this.mBaseItem)
throw Components.results.NS_ERROR_NOT_INITIALIZED;
if (this.mImmutable)
throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
if (aIndex < 0 || aIndex > this.mRecurrenceItems.length)
throw Components.results.NS_ERROR_INVALID_ARG;
this.mRecurrenceItems.splice(aIndex, 0, aItem);
},
clearRecurrenceItems: function() {
if (!this.mBaseItem)
throw Components.results.NS_ERROR_NOT_INITIALIZED;
if (this.mImmutable)
throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
this.mRecurrenceItems = new Array();
},
//
// calculations
//
getNextOccurrenceDate: function (aTime) {
if (!this.mBaseItem)
throw Components.results.NS_ERROR_NOT_INITIALIZED;
var startDate = this.mBaseItem.recurrenceStartDate;
var dates = [];
for each (ritem in this.mRecurrenceItems) {
var date = ritem.getNextOccurrence(startDate, aTime);
if (!date)
continue;
if (ritem.isNegative)
dates = dates.filter(function (d) { return (d.compare(date) != 0); });
else
dates.push(date);
}
// if no dates, there's no next
if (dates.length == 0)
return null;
// find the earliest date
var earliestDate = dates[0];
dates.forEach(function (d) { if (d.compare(earliestDate) < 0) earliestDate = d; });
return earliestDate;
},
getNextOccurrence: function (aTime) {
var earliestDate = this.getNextOccurrenceDate (aTime);
if (!earliestDate)
return null;
if (this.mExceptions) {
// scan exceptions for any dates earlier than
// earliestDate (but still after aTime)
this.mExceptions.forEach (function (ex) {
var dtstart = ex.item.getProperty("DTSTART");
if (aTime.compare(dtstart) <= 0 &&
earliestDate.compare(dtstart) > 0)
{
earliestDate = dtstart;
}
});
}
var startDate = earliestDate.clone();
var endDate = null;
if (this.mBaseItem.hasProperty("DTEND")) {
endDate = earliestDate.clone();
endDate.addDuration(this.mBaseItem.duration);
}
var proxy = this.mBaseItem.createProxy();
proxy.recurrenceId = earliestDate.clone();
proxy.setProperty("DTSTART", startDate);
if (endDate)
proxy.setProperty("DTEND", endDate);
return proxy;
},
// internal helper function;
calculateDates: function (aRangeStart, aRangeEnd,
aMaxCount, aReturnRIDs)
{
if (!this.mBaseItem)
throw Components.results.NS_ERROR_NOT_INITIALIZED;
// workaround for UTC- timezones
var rangeStart = ensureDateTime(aRangeStart);
var rangeEnd = ensureDateTime(aRangeEnd);
// If aRangeStart falls in the middle of an occurrence, libical will
// not return that occurrence when we go and ask for an
// icalrecur_iterator_new. This actually seems fairly rational, so
// instead of hacking libical, I'm going to move aRangeStart back far
// enough to make sure we get the occurrences we might miss.
var searchStart = rangeStart.clone();
var baseDuration = null;
try {
baseDuration = this.mBaseItem.duration;
var duration = baseDuration.clone();
duration.isNegative = true;
searchStart.addDuration(duration);
} catch(ex) {
dump("recurrence tweaking exception:"+ex+'\n');
}
var startDate = this.mBaseItem.recurrenceStartDate;
var dates = [];
// DTSTART/DUE is always part of the (positive) expanded set:
// the base item cannot be replaced by an exception;
// an exception can only be defined on an item resulting from an RDATE/RRULE;
// DTSTART always equals RECURRENCE-ID for items expanded from RRULE
var baseOccDate = checkIfInRange(this.mBaseItem, aRangeStart, aRangeEnd, true);
if (baseOccDate) {
dates.push(baseOccDate);
}
// toss in exceptions first:
if (this.mExceptions) {
this.mExceptions.forEach(
function(ex) {
var occDate = checkIfInRange(ex.item, aRangeStart, aRangeEnd, true);
if (occDate) {
dates.push(aReturnRIDs ? ex.id : occDate);
}
});
}
// if both range start and end are specified, we ask for all of the occurrences,
// to make sure we catch all possible exceptions. If aRangeEnd isn't specified,
// then we have to ask for aMaxCount, and hope for the best.
var maxCount;
if (rangeStart && rangeEnd) {
maxCount = 0;
} else {
maxCount = aMaxCount;
}
// apply positive items before negative:
var sortedRecurrenceItems = [];
for each ( var ritem in this.mRecurrenceItems ) {
if (ritem.isNegative)
sortedRecurrenceItems.push(ritem);
else
sortedRecurrenceItems.unshift(ritem);
}
for each (ritem in sortedRecurrenceItems) {
var cur_dates;
cur_dates = ritem.getOccurrences(startDate,
searchStart,
rangeEnd,
maxCount, {});
if (cur_dates.length == 0)
continue;
if (ritem.isNegative) {
// if this is negative, we look for any of the given dates
// in the existing set, and remove them if they're
// present.
// XXX: i'm pretty sure negative dates can't really have exceptions
// (like, you can't make a date "real" by defining an RECURRENCE-ID which
// is an EXDATE, and then giving it a real DTSTART) -- so we don't
// check exceptions here
cur_dates.forEach (function (dateToRemove) {
dates = dates.filter(function (d) { return d.compare(dateToRemove) != 0; });
});
} else {
// if positive, we just add these date to the existing set,
// but only if they're not already there
var index = 0;
const len = cur_dates.length;
// skip items before rangeStart due to searchStart libical hack:
if (rangeStart && baseDuration) {
for (; index < len; ++index) {
var date = cur_dates[index].clone();
date.addDuration(baseDuration);
if (rangeStart.compare(date) < 0) {
break;
}
}
}
for (; index < len; ++index) {
var dateToAdd = cur_dates[index];
if (!dates.some(function (d) { return d.compare(dateToAdd) == 0; })) {
dates.push(dateToAdd);
}
}
}
}
// now sort the list
dates.sort(function (a,b) { return a.compare(b); });
// chop anything over aMaxCount, if specified
if (aMaxCount && dates.length > aMaxCount)
dates = dates.splice(aMaxCount, dates.length - aMaxCount);
return dates;
},
getOccurrenceDates: function (aRangeStart, aRangeEnd,
aMaxCount, aCount)
{
var dates = this.calculateDates(aRangeStart, aRangeEnd, aMaxCount, false);
aCount.value = dates.length;
return dates;
},
getOccurrences: function (aRangeStart, aRangeEnd,
aMaxCount,
aCount)
{
var dates = this.calculateDates(aRangeStart, aRangeEnd, aMaxCount, true);
if (dates.length == 0) {
aCount.value = 0;
return [];
}
var count = aMaxCount;
if (!count)
count = dates.length;
var results = [];
for (var i = 0; i < count; i++) {
var proxy = this.getOccurrenceFor(dates[i]);
results.push(proxy);
}
aCount.value = results.length;
return results;
},
getOccurrenceFor: function (aRecurrenceId) {
var proxy = this.getExceptionFor(aRecurrenceId, false);
if (!proxy) {
var duration = null;
var name = "DTEND";
if (this.mBaseItem instanceof Components.interfaces.calITodo)
name = "DUE";
if (this.mBaseItem.hasProperty(name))
duration = this.mBaseItem.duration;
proxy = this.mBaseItem.createProxy();
proxy.recurrenceId = aRecurrenceId;
proxy.setProperty("DTSTART", aRecurrenceId.clone());
if (duration) {
var enddate = aRecurrenceId.clone();
enddate.addDuration(duration);
proxy.setProperty(name, enddate);
}
if (!this.mBaseItem.isMutable)
proxy.makeImmutable();
}
return proxy;
},
removeOccurrenceAt: function (aRecurrenceId) {
if (!this.mBaseItem)
throw Components.results.NS_ERROR_NOT_INITIALIZED;
if (this.mImmutable)
throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
var d = Components.classes["@mozilla.org/calendar/recurrence-date;1"].createInstance(Components.interfaces.calIRecurrenceDate);
d.isNegative = true;
d.date = aRecurrenceId.clone();
this.removeExceptionFor(d.date);
this.appendRecurrenceItem(d);
},
restoreOccurrenceAt: function (aRecurrenceId) {
if (!this.mBaseItem)
throw Components.results.NS_ERROR_NOT_INITIALIZED;
if (this.mImmutable)
throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
for (var i = 0; i < this.mRecurrenceItems.length; i++) {
if (this.mRecurrenceItems[i] instanceof Components.interfaces.calIRecurrenceDate) {
var rd = this.mRecurrenceItems[i].QueryInterface(Components.interfaces.calIRecurrenceDate);
if (rd.isNegative && rd.date.compare(aRecurrenceId) == 0) {
return this.deleteRecurrenceItemAt(i);
}
}
}
throw Components.results.NS_ERROR_INVALID_ARG;
},
//
// exceptions
//
//
// Some notes:
//
// The way I read ICAL, RECURRENCE-ID is used to specify a
// particular instance of a recurring event, according to the
// RRULEs/RDATEs/etc. specified in the base event. If one of
// these is to be changed ("an exception"), then it can be
// referenced via the UID of the original event, and a
// RECURRENCE-ID of the start time of the instance to change.
// This, to me, means that an event where one of the instances has
// changed to a different time has a RECURRENCE-ID of the original
// start time, and a DTSTART/DTEND representing the new time.
//
// ITIP, however, seems to want something different -- you're
// supposed to use UID/RECURRENCE-ID to select from the current
// set of occurrences of an event. If you change the DTSTART for
// an instance, you're supposed to use the old (original) DTSTART
// as the RECURRENCE-ID, and put the new time as the DTSTART.
// However, after that change, to refer to that instance in the
// future, you have to use the modified DTSTART as the
// RECURRENCE-ID. This madness is described in ITIP end of
// section 3.7.1.
//
// This implementation does the first approach (RECURRENCE-ID will
// never change even if DTSTART for that instance changes), which
// I think is the right thing to do for CalDAV; I don't know what
// we'll do for incoming ITIP events though.
//
mExceptions: null,
modifyException: function (anItem) {
if (!this.mBaseItem)
throw Components.results.NS_ERROR_NOT_INITIALIZED;
// the item must be an occurrence
if (anItem.parentItem == anItem)
throw Components.results.NS_ERROR_UNEXPECTED;
if (anItem.parentItem.calendar != this.mBaseItem.calendar &&
anItem.parentItem.id != this.mBaseItem.id)
{
calDebug ("recurrenceInfo::addException: item parentItem != this.mBaseItem (calendar/id)!\n");
throw Components.results.NS_ERROR_INVALID_ARG;
}
if (anItem.recurrenceId == null) {
calDebug ("recurrenceInfo::addException: item with null recurrenceId!\n");
throw Components.results.NS_ERROR_INVALID_ARG;
}
var itemtoadd;
if (anItem.isMutable) {
itemtoadd = anItem.cloneShallow(this.mBaseItem);
itemtoadd.makeImmutable();
} else {
itemtoadd = anItem;
}
// we're going to assume that the recurrenceId is valid here,
// because presumably the item came from one of our functions
// remove any old one, if present
this.removeExceptionFor(anItem.recurrenceId);
this.mExceptions.push( { id: itemtoadd.recurrenceId, item: itemtoadd } );
},
createExceptionFor: function (aRecurrenceId) {
if (!this.mBaseItem)
throw Components.results.NS_ERROR_NOT_INITIALIZED;
// XX should it be an error to createExceptionFor
// an already-existing recurrenceId?
var existing = this.getExceptionFor(aRecurrenceId, false);
if (existing)
return existing;
// check if aRecurrenceId is valid.
// this is a bit of a hack; we know that ranges are defined as [start, end),
// so we do a search on aRecurrenceId and aRecurrenceId.seconds + 1.
var rangeStart = aRecurrenceId;
var rangeEnd = aRecurrenceId.clone();
rangeEnd.second += 1;
var dates = this.getOccurrenceDates (rangeStart, rangeEnd, 1, {});
var found = false;
for each (d in dates) {
if (d.compare(aRecurrenceId) == 0) {
found = true;
break;
}
}
// not found; the recurrence id is invalid
if (!found)
throw Components.results.NS_ERROR_INVALID_ARG;
var rid = aRecurrenceId.clone();
rid.makeImmutable();
var newex = this.mBaseItem.createProxy();
newex.recurrenceId = rid;
this.mExceptions.push({id: rid, item: newex});
return newex;
},
getExceptionFor: function (aRecurrenceId, aCreate) {
if (!this.mBaseItem)
throw Components.results.NS_ERROR_NOT_INITIALIZED;
for each (ex in this.mExceptions) {
if (ex.id.compare(aRecurrenceId) == 0)
return ex.item;
}
if (aCreate) {
return this.createExceptionFor(aRecurrenceId);
}
return null;
},
removeExceptionFor: function (aRecurrenceId) {
if (!this.mBaseItem)
throw Components.results.NS_ERROR_NOT_INITIALIZED;
this.mExceptions = this.mExceptions.filter (function(ex) {
return (ex.id.compare(aRecurrenceId) != 0);
});
},
getExceptionIds: function (aCount) {
if (!this.mBaseItem)
throw Components.results.NS_ERROR_NOT_INITIALIZED;
var ids = this.mExceptions.map (function(ex) {
return ex.id;
});
aCount.value = ids.length;
return ids;
},
// changing the startdate of an item needs to take exceptions into account.
// in case we're about to modify a parentItem (aka 'folded' item), we need
// to modify the recurrenceId's of all possibly existing exceptions as well.
onStartDateChange: function (aNewStartTime, aOldStartTime) {
// passing null for the new starttime would indicate an error condition,
// since having a recurrence without a starttime is invalid.
if (!aNewStartTime) {
throw Components.results.NS_ERROR_INVALID_ARG;
}
// no need to check for changes if there's no previous starttime.
if (!aOldStartTime) {
return;
}
// convert both dates to UTC since subtractDate is not timezone aware.
aOldStartTime = aOldStartTime.getInTimezone(UTC());
aNewStartTime = aNewStartTime.getInTimezone(UTC());
var timeDiff = aNewStartTime.subtractDate(aOldStartTime);
var exceptions = this.getExceptionIds({});
var modifiedExceptions = [];
for each (var exid in exceptions) {
var ex = this.getExceptionFor(exid, false);
if (ex) {
if (!ex.isMutable) {
ex = ex.cloneShallow(this.item);
}
ex.recurrenceId.addDuration(timeDiff);
modifiedExceptions.push(ex);
this.removeExceptionFor(exid);
}
}
for each (var modifiedEx in modifiedExceptions) {
this.modifyException(modifiedEx);
}
// also take RDATE's and EXDATE's into account.
const kCalIRecurrenceDate = Components.interfaces.calIRecurrenceDate;
const kCalIRecurrenceDateSet = Components.interfaces.calIRecurrenceDateSet;
var ritems = this.getRecurrenceItems({});
for (var i in ritems) {
var ritem = ritems[i];
if (ritem instanceof kCalIRecurrenceDate) {
ritem = ritem.QueryInterface(kCalIRecurrenceDate);
ritem.date.addDuration(timeDiff);
} else if (ritem instanceof kCalIRecurrenceDateSet) {
ritem = ritem.QueryInterface(kCalIRecurrenceDateSet);
var rdates = ritem.getDates({});
for each (var date in rdates) {
date.addDuration(timeDiff);
}
ritem.setDates(rdates.length,rdates);
}
}
}
};